package view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.Timer;
import simulation.Factory;
import simulation.FixedMass;
import simulation.Force;
import simulation.Mass;
import simulation.Model;
import simulation.Spring;
import simulation.SpringieKeyAction;
import util.Vector;


/**
 * Creates an area of the screen in which the game will be drawn that supports:
 * <UL>
 *   <LI>animation via the Timer
 *   <LI>mouse input via the MouseListener and MouseMotionListener
 *   <LI>keyboard input via the KeyListener
 * </UL>
 * 
 * @author Robert C Duvall
 */
public class Canvas extends JComponent {
    // animate 25 times per second if possible
    /** */
    public static final int FRAMES_PER_SECOND = 25;
    // better way to think about timed events (in milliseconds)
    /** */
    public static final int ONE_SECOND = 1000;
    /** */
    public static final int DEFAULT_DELAY = ONE_SECOND / FRAMES_PER_SECOND;
    /** */
    public static final int NO_KEY_PRESSED = -1;
    /** */
    public static final Point NO_MOUSE_PRESSED = null;
    /** */
    public static final int SHIFT_AMOUNT = 10;
    /** */
    public static final int GRAB_SPRING_K = 50;
    // only one so that it maintains user's preferences
    private static final JFileChooser INPUT_CHOOSER = 
            new JFileChooser(System.getProperties().getProperty("user.dir"));
    // default serialization ID
    private static final long serialVersionUID = 1L;
    // input state
    //keys
    private static final int CLEAR_ASSEMBLIES = KeyEvent.VK_C;
    private static final int NEW_ASSEMBLY = KeyEvent.VK_N;
    private static final int UP_ARROW = KeyEvent.VK_UP;
    private static final int DOWN_ARROW = KeyEvent.VK_DOWN;
    private static final int LEFT_ARROW = KeyEvent.VK_LEFT;
    private static final int RIGHT_ARROW = KeyEvent.VK_RIGHT;
    private static final int GRAVITY_TOGGLE = KeyEvent.VK_G;
    private static final int VISCOSITY_TOGGLE = KeyEvent.VK_V;
    private static final int CENTER_OF_MASS_TOGGLE = KeyEvent.VK_M;
    private static final int WALL_1 = KeyEvent.VK_1;
    private static final int WALL_2 = KeyEvent.VK_2;
    private static final int WALL_3 = KeyEvent.VK_3;
    private static final int WALL_4 = KeyEvent.VK_4;
    // drives the animation
    private Timer myTimer;
    // game to be animated
    private Model mySimulation;
    // input state
    private int myLastKeyPressed;
    private Point myLastMousePosition;
    private Set<Integer> myKeys;
    private Factory myFactory;
    private Spring myMouseSpring;
    private FixedMass myMouseMass;
    private ArrayList<SpringieKeyAction> myKeyActions = new ArrayList<SpringieKeyAction>();

    /**
     * Create a panel so that it knows its size
     * @param size of canvas
     */
    public Canvas (Dimension size) {
        setPreferredSize(size);
        setSize(size);
        setFocusable(true);
        requestFocus();
        setInputListeners();
        myMouseMass = new FixedMass(0, 0, 0, mySimulation);
        createNewAssembly();
        clearAssemblies();
        changeSizeOfWalledArea();
        toggleGravity();
        toggleViscosity();
        toggleCenterOfMass();
        toggleWallRepulsion();
    }

    private void changeSizeOfWalledArea () {
        myKeyActions.add(new SpringieKeyAction(UP_ARROW) {
            @Override
            public void act() {
                mySimulation.setHeight(mySimulation.getHeight() + SHIFT_AMOUNT);
                mySimulation.setWidth(mySimulation.getWidth() + SHIFT_AMOUNT);
                mySimulation.moveMasses();
            }
        });
        myKeyActions.add(new SpringieKeyAction(DOWN_ARROW) {
            @Override
            public void act() {
                mySimulation.setHeight(mySimulation.getHeight() - SHIFT_AMOUNT);
                mySimulation.setWidth(mySimulation.getWidth() - SHIFT_AMOUNT);
                mySimulation.moveMasses();
            }
        });
        myKeyActions.add(new SpringieKeyAction(LEFT_ARROW) {
            @Override
            public void act() {
                mySimulation.setWidth(mySimulation.getWidth() - SHIFT_AMOUNT);
                mySimulation.moveMasses();
            }
        });
        myKeyActions.add(new SpringieKeyAction(RIGHT_ARROW) {
            @Override
            public void act() {
                mySimulation.setWidth(mySimulation.getWidth() + SHIFT_AMOUNT);
                mySimulation.moveMasses();
            }
        });
    }

    private void clearAssemblies () {
        myKeyActions.add(new SpringieKeyAction(CLEAR_ASSEMBLIES) {
            @Override
            public void act() {
                mySimulation.clear();
            }
        });
    }
    
    private void toggleGravity () {
        myKeyActions.add(new SpringieKeyAction(GRAVITY_TOGGLE) {
            @Override
            public void act() {
                toggleForce(Factory.GRAVITY_INDEX);
            }
        });
    }
        
    private void toggleViscosity () {
        myKeyActions.add(new SpringieKeyAction(VISCOSITY_TOGGLE) {
            @Override
            public void act() {
                toggleForce(Factory.VISCOSITY_INDEX);
            }
        });
    }
    
    private void toggleCenterOfMass () {
        myKeyActions.add(new SpringieKeyAction(CENTER_OF_MASS_TOGGLE) {
            @Override
            public void act() {
                toggleForce(Factory.CENTER_OF_MASS_INDEX);
            }
        });
    }
    
    private void toggleForce (int forceIndex) {
        for (Force f : mySimulation.getForce()) {
            if (f.getIndex() == forceIndex) {
                f.toggle();
            }
        }
    }
    
    private void toggleWallRepulsion () {
        myKeyActions.add(new SpringieKeyAction(WALL_1) {
            @Override
            public void act() {
                toggleWall(Factory.TOP_WALL);
            }
        });
        myKeyActions.add(new SpringieKeyAction(WALL_2) {
            @Override
            public void act() {
                toggleWall(Factory.RIGHT_WALL);
            }
        });
        myKeyActions.add(new SpringieKeyAction(WALL_3) {
            @Override
            public void act() {
                toggleWall(Factory.BOTTOM_WALL);

            }
        });
        myKeyActions.add(new SpringieKeyAction(WALL_4) {
            @Override
            public void act() {
                toggleWall(Factory.LEFT_WALL);

            }
        });
    }
    
    private void toggleWall(int wallID) {
        for (Force f : mySimulation.getForce()) {
            if (f.getIndex() == Factory.WALL_REPULSION_INDEX) {
                f.toggle(wallID);
            }
        }
    }

    private void createNewAssembly () {
        myKeyActions.add(new SpringieKeyAction(NEW_ASSEMBLY) {
            @Override
            public void act() {
                loadModel();
            }
        });
    }

    /**
     * Paint the contents of the canvas.
     * 
     * Never called by you directly, instead called by Java runtime
     * when area of screen covered by this container needs to be
     * displayed (i.e., creation, uncovering, change in status)
     * 
     * @param pen used to paint shape on the screen
     */
    @Override
    public void paintComponent (Graphics pen) {
        pen.setColor(Color.WHITE);
        pen.fillRect(0, 0, getSize().width, getSize().height);
        // first time needs to be special cased :(
        pen.setColor(Color.GRAY);
        pen.fillRect(0, 0, mySimulation.getWidth(), mySimulation.getHeight());
        if (mySimulation != null) {
            mySimulation.paint((Graphics2D) pen);
        }
    }

    /**
     * Returns last key pressed by the user or -1 if nothing is pressed.
     */
    public int getLastKeyPressed () {
        return myLastKeyPressed;
    }

    /**
     * Returns all keys currently pressed by the user.
     */
    public Collection<Integer> getKeysPressed () {
        return Collections.unmodifiableSet(myKeys);
    }

    /**
     * Returns last position of the mouse in the canvas.
     */
    public Point getLastMousePosition () {
        return myLastMousePosition;
    }

    /**
     * Start the animation.
     */
    public void start () {
        // create a timer to animate the canvas
        myTimer = new Timer(DEFAULT_DELAY, 
            new ActionListener() {
                public void actionPerformed (ActionEvent e) {
                    step();
                }
            });
        // start animation
        mySimulation = new Model(this);
        myFactory = new Factory(mySimulation);
        loadModel();
        loadModel();
        myTimer.start();
    }

    /**
     * Stop the animation.
     */
    public void stop () {
        myTimer.stop();
    }

    /**
     * Take one step in the animation.
     */
    public void step () {
        mySimulation.update((double)FRAMES_PER_SECOND / ONE_SECOND);
        // indirectly causes paint to be called
        repaint();
    }
    
    /**
     * Create listeners that will update state based on user input.
     */
    private void setInputListeners () {
        // initialize input state
        myLastKeyPressed = NO_KEY_PRESSED;
        myKeys = new TreeSet<Integer>();
        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed (KeyEvent e) {
                myLastKeyPressed = e.getKeyCode();
                myKeys.add(e.getKeyCode());
            }
            @Override
            public void keyReleased (KeyEvent e) {
                for (SpringieKeyAction ka : myKeyActions) {
                    if (myKeys.contains(ka.getMyKey())) {
                        ka.act();
                    }
                }
                myLastKeyPressed = NO_KEY_PRESSED;
                myKeys.remove((Integer)e.getKeyCode());
            }
        });
        myLastMousePosition = NO_MOUSE_PRESSED;
        addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseDragged (MouseEvent e) {
                myLastMousePosition = e.getPoint();
                myMouseMass.setCenter(e.getX(), e.getY());
                //move spring attachment
            }
        });
        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed (MouseEvent e) {
                myLastMousePosition = e.getPoint();
                Mass closest = mySimulation.closestMass(myLastMousePosition);
                myMouseMass.setCenter(e.getX(), e.getY());
                myMouseSpring = new Spring(closest, myMouseMass, 
                                           Vector.distanceBetween(closest.getCenter(),
                                                                  myLastMousePosition),
                                           GRAB_SPRING_K);
                mySimulation.add(myMouseSpring);
            }
            
            @Override
            public void mouseReleased (MouseEvent e) {
                myLastMousePosition = NO_MOUSE_PRESSED;
                mySimulation.remove(myMouseSpring);
            }
        });
    }

    // load model from file chosen by user
    private void loadModel () {
        int response = INPUT_CHOOSER.showOpenDialog(null);
        if (response == JFileChooser.APPROVE_OPTION) {
            myFactory.load(mySimulation, INPUT_CHOOSER.getSelectedFile());
        }
    }
}
